Для развлекательного приложения Procrastinate Pro+ необходимо провести маркетинговый анализ. Выявить причины убытков в последние несколько месяцев, несмотря на огромные вложения в рекламу. Нам надо разобраться в причинах и помочь компании выйти в плюс.
Были предоставлены лог сервера с данными об их посещениях, привлечённых с 1 мая по 27 октября 2019 года с выгрузкой их покупок за этот период и рекламные расходы.
Изучим откуда приходят пользователи и какими устройствами они пользуются, сколько стоит привлечение пользователей из различных рекламных каналов , сколько денег приносит каждый клиент, когда расходы на привлечение клиента окупаются, какие факторы мешают привлечению клиентов.
Описание данных
В вашем распоряжении три датасета. Файл visits_info_short.csv хранит лог сервера с информацией о посещениях сайта, orders_info_short.csv — информацию о заказах, а costs_info_short.csv — информацию о расходах на рекламу.
Структура visits_info_short.csv:
Структура orders_info_short.csv:
Структура costs_info_short.csv:dt — дата проведения рекламной кампании,
# импортируем библиотеки
import pandas as pd
from datetime import datetime, timedelta
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
# загружаем данные о визитах, заказах и рекламных расходах из CSV-файлов в переменные.
try:
visits = pd.read_csv(r"B:\Downloads\visits_info_short.csv", sep = ',') # лог сервера с информацией о посещениях сайта
orders = pd.read_csv(r"B:\Downloads\orders_info_short.csv", sep = ',') # информацию о заказах
costs = pd.read_csv(r"B:\Downloads\costs_info_short.csv", sep = ',') # информацию о расходах на рекламу
except:
visits = pd.read_csv("/datasets/visits_info_short.csv", sep = ',')
orders = pd.read_csv("/datasets/orders_info_short.csv", sep = ',')
costs = pd.read_csv("/datasets/costs_info_short.csv", sep = ',')
#выведем 10 строк таблицы visits, orders, costs
display(visits.head(10))
display(orders.head(10))
costs.head(10)
| User Id | Region | Device | Channel | Session Start | Session End | |
|---|---|---|---|---|---|---|
| 0 | 981449118918 | United States | iPhone | organic | 2019-05-01 02:36:01 | 2019-05-01 02:45:01 |
| 1 | 278965908054 | United States | iPhone | organic | 2019-05-01 04:46:31 | 2019-05-01 04:47:35 |
| 2 | 590706206550 | United States | Mac | organic | 2019-05-01 14:09:25 | 2019-05-01 15:32:08 |
| 3 | 326433527971 | United States | Android | TipTop | 2019-05-01 00:29:59 | 2019-05-01 00:54:25 |
| 4 | 349773784594 | United States | Mac | organic | 2019-05-01 03:33:35 | 2019-05-01 03:57:40 |
| 5 | 90242400005 | United States | iPhone | RocketSuperAds | 2019-05-01 07:14:52 | 2019-05-01 08:19:56 |
| 6 | 43958116050 | United States | Android | organic | 2019-05-01 09:03:21 | 2019-05-01 10:08:39 |
| 7 | 185365244969 | United States | iPhone | organic | 2019-05-01 09:37:03 | 2019-05-01 10:00:43 |
| 8 | 446013509831 | United States | iPhone | RocketSuperAds | 2019-05-01 10:26:00 | 2019-05-01 10:59:17 |
| 9 | 100970711362 | United States | Mac | TipTop | 2019-05-01 04:39:53 | 2019-05-01 04:57:05 |
| User Id | Event Dt | Revenue | |
|---|---|---|---|
| 0 | 188246423999 | 2019-05-01 23:09:52 | 4.99 |
| 1 | 174361394180 | 2019-05-01 12:24:04 | 4.99 |
| 2 | 529610067795 | 2019-05-01 11:34:04 | 4.99 |
| 3 | 319939546352 | 2019-05-01 15:34:40 | 4.99 |
| 4 | 366000285810 | 2019-05-01 13:59:51 | 4.99 |
| 5 | 129100331057 | 2019-05-01 01:56:28 | 4.99 |
| 6 | 626807144131 | 2019-05-01 02:39:21 | 4.99 |
| 7 | 914062168985 | 2019-05-01 02:02:18 | 4.99 |
| 8 | 769669137453 | 2019-05-01 03:39:47 | 4.99 |
| 9 | 434198630691 | 2019-05-01 18:16:50 | 5.99 |
| dt | Channel | costs | |
|---|---|---|---|
| 0 | 2019-05-01 | FaceBoom | 113.3 |
| 1 | 2019-05-02 | FaceBoom | 78.1 |
| 2 | 2019-05-03 | FaceBoom | 85.8 |
| 3 | 2019-05-04 | FaceBoom | 136.4 |
| 4 | 2019-05-05 | FaceBoom | 122.1 |
| 5 | 2019-05-06 | FaceBoom | 118.8 |
| 6 | 2019-05-07 | FaceBoom | 101.2 |
| 7 | 2019-05-08 | FaceBoom | 100.1 |
| 8 | 2019-05-09 | FaceBoom | 93.5 |
| 9 | 2019-05-10 | FaceBoom | 104.5 |
# выведем количество строк и столбцов в таблицах
display(visits.shape)
display(orders.shape)
costs.shape
(309901, 6)
(40212, 3)
(1800, 3)
# выведем статистические показатели в таблицах visits, orders, costs
display(orders.describe())
display(visits.describe())
costs.describe()
| User Id | Revenue | |
|---|---|---|
| count | 4.021200e+04 | 40212.000000 |
| mean | 4.990295e+11 | 5.370608 |
| std | 2.860937e+11 | 3.454208 |
| min | 5.993260e+05 | 4.990000 |
| 25% | 2.511324e+11 | 4.990000 |
| 50% | 4.982840e+11 | 4.990000 |
| 75% | 7.433327e+11 | 4.990000 |
| max | 9.998954e+11 | 49.990000 |
| User Id | |
|---|---|
| count | 3.099010e+05 |
| mean | 4.997664e+11 |
| std | 2.887899e+11 |
| min | 5.993260e+05 |
| 25% | 2.493691e+11 |
| 50% | 4.989906e+11 |
| 75% | 7.495211e+11 |
| max | 9.999996e+11 |
| costs | |
|---|---|
| count | 1800.000000 |
| mean | 58.609611 |
| std | 107.740223 |
| min | 0.800000 |
| 25% | 6.495000 |
| 50% | 12.285000 |
| 75% | 33.600000 |
| max | 630.000000 |
# выведем общую информацию о данных в таблицах visits, orders, costs
display(orders.info())
display(visits.info())
costs.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 40212 entries, 0 to 40211 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 User Id 40212 non-null int64 1 Event Dt 40212 non-null object 2 Revenue 40212 non-null float64 dtypes: float64(1), int64(1), object(1) memory usage: 942.6+ KB
None
<class 'pandas.core.frame.DataFrame'> RangeIndex: 309901 entries, 0 to 309900 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 User Id 309901 non-null int64 1 Region 309901 non-null object 2 Device 309901 non-null object 3 Channel 309901 non-null object 4 Session Start 309901 non-null object 5 Session End 309901 non-null object dtypes: int64(1), object(5) memory usage: 14.2+ MB
None
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1800 entries, 0 to 1799 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 dt 1800 non-null object 1 Channel 1800 non-null object 2 costs 1800 non-null float64 dtypes: float64(1), object(2) memory usage: 42.3+ KB
# посчитаем количество дубликатов в таблицах visits, orders, costs
display(visits.duplicated().sum())
display(orders.duplicated().sum())
costs.duplicated().sum()
0
0
0
# заменим пробелы в названиях столбцов на нижнее подчеркивание в таблицах visits, orders, costs
visits.columns = [x.lower().replace(' ', '_') for x in visits.columns]
orders.columns = [x.lower().replace(' ', '_') for x in orders.columns]
costs.columns = costs.columns.str.lower()
visits.columns, orders.columns, costs.columns
(Index(['user_id', 'region', 'device', 'channel', 'session_start',
'session_end'],
dtype='object'),
Index(['user_id', 'event_dt', 'revenue'], dtype='object'),
Index(['dt', 'channel', 'costs'], dtype='object'))
# уникальные значения столбца region
visits.region.value_counts()
United States 207327 UK 36419 France 35396 Germany 30759 Name: region, dtype: int64
# уникальные значения столбца device
visits.device.value_counts()
iPhone 112603 Android 72590 PC 62686 Mac 62022 Name: device, dtype: int64
# уникальные значения столбца channel
visits.channel.value_counts()
organic 107760 TipTop 54794 FaceBoom 49022 WahooNetBanner 20465 LeapBob 17013 OppleCreativeMedia 16794 RocketSuperAds 12724 YRabbit 9053 MediaTornado 8878 AdNonSense 6891 lambdaMediaAds 6507 Name: channel, dtype: int64
# приводим данные о времени к формату datetime
visits['session_start'] = pd.to_datetime(visits['session_start'])
orders['event_dt'] = pd.to_datetime(orders['event_dt'])
costs['dt'] = pd.to_datetime(costs['dt']).dt.date
Для исследования загрузили 3 таблицы:
В анализе учавствуют 4 страны: United States(США), UK(Великобритания), France(Франция), Germany(Германия), разделенные на категории устройств пользователей iPhone, Android, PC, Mac на разных каналах.
Дубликаты и пропуски в данных отсутствуют. Заменили в таблицах пробелы в названиях столбцов на нижнее подчеркивание и перевели в нижний регистр. Преобразовали данные о времени в таблицах к формату datetime.
Задаем функции для вычисления значений метрик:
# функция для создания пользовательских профилей
def get_profiles(sessions, orders, ad_costs, event_names=[]):
# находим параметры первых посещений
profiles = (
sessions.sort_values(by=['user_id', 'session_start'])
.groupby('user_id')
.agg(
{
'session_start': 'first',
'channel': 'first',
'device': 'first',
'region': 'first',
}
)
.rename(columns={'session_start': 'first_ts'})
.reset_index()
)
# для когортного анализа определяем дату первого посещения
# и первый день месяца, в который это посещение произошло
profiles['dt'] = profiles['first_ts'].dt.date
profiles['month'] = profiles['first_ts'].astype('datetime64[M]')
# добавляем признак платящих пользователей
profiles['payer'] = profiles['user_id'].isin(orders['user_id'].unique())
# считаем количество уникальных пользователей
# с одинаковыми источником и датой привлечения
new_users = (
profiles.groupby(['dt', 'channel'])
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'unique_users'})
.reset_index()
)
# объединяем траты на рекламу и число привлечённых пользователей
ad_costs = ad_costs.merge(new_users, on=['dt', 'channel'], how='left')
# делим рекламные расходы на число привлечённых пользователей
ad_costs['acquisition_cost'] = ad_costs['costs'] / ad_costs['unique_users']
# добавляем стоимость привлечения в профили
profiles = profiles.merge(
ad_costs[['dt', 'channel', 'acquisition_cost']],
on=['dt', 'channel'],
how='left',
)
# стоимость привлечения органических пользователей равна нулю
profiles['acquisition_cost'] = profiles['acquisition_cost'].fillna(0)
return profiles
# функция для расчёта Retention Rate(удержания)
def get_retention(
profiles,
sessions,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# добавляем столбец payer в передаваемый dimensions список
dimensions = ['payer'] + dimensions
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# собираем «сырые» данные для расчёта удержания
result_raw = result_raw.merge(
sessions[['user_id', 'session_start']], on='user_id', how='left'
)
result_raw['lifetime'] = (
result_raw['session_start'] - result_raw['first_ts']
).dt.days
# функция для группировки таблицы по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
result = df.pivot_table(
index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
)
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
result = result.div(result['cohort_size'], axis=0)
result = result[['cohort_size'] + list(range(horizon_days))]
result['cohort_size'] = cohort_sizes
return result
# получаем таблицу удержания
result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)
# получаем таблицу динамики удержания
result_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
# возвращаем обе таблицы и сырые данные
return result_raw, result_grouped, result_in_time
# функция для расчёта конверсии
def get_conversion(
profiles,
purchases,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# определяем дату и время первой покупки для каждого пользователя
first_purchases = (
purchases.sort_values(by=['user_id', 'event_dt'])
.groupby('user_id')
.agg({'event_dt': 'first'})
.reset_index()
)
# добавляем данные о покупках в профили
result_raw = result_raw.merge(
first_purchases[['user_id', 'event_dt']], on='user_id', how='left'
)
# рассчитываем лайфтайм для каждой покупки
result_raw['lifetime'] = (
result_raw['event_dt'] - result_raw['first_ts']
).dt.days
# группируем по cohort, если в dimensions ничего нет
if len(dimensions) == 0:
result_raw['cohort'] = 'All users'
dimensions = dimensions + ['cohort']
# функция для группировки таблицы по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
result = df.pivot_table(
index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
)
result = result.fillna(0).cumsum(axis = 1)
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
# делим каждую «ячейку» в строке на размер когорты
# и получаем conversion rate
result = result.div(result['cohort_size'], axis=0)
result = result[['cohort_size'] + list(range(horizon_days))]
result['cohort_size'] = cohort_sizes
return result
# получаем таблицу конверсии
result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)
# для таблицы динамики конверсии убираем 'cohort' из dimensions
if 'cohort' in dimensions:
dimensions = []
# получаем таблицу динамики конверсии
result_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
# возвращаем обе таблицы и сырые данные
return result_raw, result_grouped, result_in_time
# функция для расчёта LTV и ROI
def get_ltv(
profiles,
purchases,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# добавляем данные о покупках в профили
result_raw = result_raw.merge(
purchases[['user_id', 'event_dt', 'revenue']], on='user_id', how='left'
)
# рассчитываем лайфтайм пользователя для каждой покупки
result_raw['lifetime'] = (
result_raw['event_dt'] - result_raw['first_ts']
).dt.days
# группируем по cohort, если в dimensions ничего нет
if len(dimensions) == 0:
result_raw['cohort'] = 'All users'
dimensions = dimensions + ['cohort']
# функция группировки по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
# строим «треугольную» таблицу выручки
result = df.pivot_table(
index=dims, columns='lifetime', values='revenue', aggfunc='sum'
)
# находим сумму выручки с накоплением
result = result.fillna(0).cumsum(axis=1)
# вычисляем размеры когорт
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
# объединяем размеры когорт и таблицу выручки
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
# считаем LTV: делим каждую «ячейку» в строке на размер когорты
result = result.div(result['cohort_size'], axis=0)
# исключаем все лайфтаймы, превышающие горизонт анализа
result = result[['cohort_size'] + list(range(horizon_days))]
# восстанавливаем размеры когорт
result['cohort_size'] = cohort_sizes
# собираем датафрейм с данными пользователей и значениями CAC,
# добавляя параметры из dimensions
cac = df[['user_id', 'acquisition_cost'] + dims].drop_duplicates()
# считаем средний CAC по параметрам из dimensions
cac = (
cac.groupby(dims)
.agg({'acquisition_cost': 'mean'})
.rename(columns={'acquisition_cost': 'cac'})
)
# считаем ROI: делим LTV на CAC
roi = result.div(cac['cac'], axis=0)
# удаляем строки с бесконечным ROI
roi = roi[~roi['cohort_size'].isin([np.inf])]
# восстанавливаем размеры когорт в таблице ROI
roi['cohort_size'] = cohort_sizes
# добавляем CAC в таблицу ROI
roi['cac'] = cac['cac']
# в финальной таблице оставляем размеры когорт, CAC
# и ROI в лайфтаймы, не превышающие горизонт анализа
roi = roi[['cohort_size', 'cac'] + list(range(horizon_days))]
# возвращаем таблицы LTV и ROI
return result, roi
# получаем таблицы LTV и ROI
result_grouped, roi_grouped = group_by_dimensions(
result_raw, dimensions, horizon_days
)
# для таблиц динамики убираем 'cohort' из dimensions
if 'cohort' in dimensions:
dimensions = []
# получаем таблицы динамики LTV и ROI
result_in_time, roi_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
return (
result_raw, # сырые данные
result_grouped, # таблица LTV
result_in_time, # таблица динамики LTV
roi_grouped, # таблица ROI
roi_in_time, # таблица динамики ROI
)
Задаем функции для построения графиков:
# функция для сглаживания фрейма
def filter_data(df, window):
# для каждого столбца применяем скользящее среднее
for column in df.columns.values:
df[column] = df[column].rolling(window).mean()
return df
# функция для визуализации удержания
def plot_retention(retention, retention_history, horizon, window=7):
# задаём размер сетки для графиков
plt.figure(figsize=(15, 10))
# исключаем размеры когорт и удержание первого дня
retention = retention.drop(columns=['cohort_size', 0])
# в таблице динамики оставляем только нужный лайфтайм
retention_history = retention_history.drop(columns=['cohort_size'])[
[horizon - 1]
]
# если в индексах таблицы удержания только payer,
# добавляем второй признак — cohort
if retention.index.nlevels == 1:
retention['cohort'] = 'All users'
retention = retention.reset_index().set_index(['cohort', 'payer'])
# в таблице графиков — два столбца и две строки, четыре ячейки
# в первой строим кривые удержания платящих пользователей
ax1 = plt.subplot(2, 2, 1)
retention.query('payer == True').droplevel('payer').T.plot(
grid=True, ax=ax1
)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Удержание платящих пользователей')
# во второй ячейке строим кривые удержания неплатящих
# вертикальная ось — от графика из первой ячейки
ax2 = plt.subplot(2, 2, 2, sharey=ax1)
retention.query('payer == False').droplevel('payer').T.plot(
grid=True, ax=ax2
)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Удержание неплатящих пользователей')
# в третьей ячейке — динамика удержания платящих
ax3 = plt.subplot(2, 2, 3)
# получаем названия столбцов для сводной таблицы
columns = [
name
for name in retention_history.index.names
if name not in ['dt', 'payer']
]
# фильтруем данные и строим график
filtered_data = retention_history.query('payer == True').pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax3)
plt.xlabel('Дата привлечения')
plt.title(
'Динамика удержания платящих пользователей на {}-й день'.format(
horizon
)
)
# в чётвертой ячейке — динамика удержания неплатящих
ax4 = plt.subplot(2, 2, 4, sharey=ax3)
# фильтруем данные и строим график
filtered_data = retention_history.query('payer == False').pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax4)
plt.xlabel('Дата привлечения')
plt.title(
'Динамика удержания неплатящих пользователей на {}-й день'.format(
horizon
)
)
plt.tight_layout()
plt.show()
# функция для визуализации конверсии
def plot_conversion(conversion, conversion_history, horizon, window=7):
# задаём размер сетки для графиков
plt.figure(figsize=(15, 5))
# исключаем размеры когорт
conversion = conversion.drop(columns=['cohort_size'])
# в таблице динамики оставляем только нужный лайфтайм
conversion_history = conversion_history.drop(columns=['cohort_size'])[
[horizon - 1]
]
# первый график — кривые конверсии
ax1 = plt.subplot(1, 2, 1)
conversion.T.plot(grid=True, ax=ax1)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Конверсия пользователей')
# второй график — динамика конверсии
ax2 = plt.subplot(1, 2, 2, sharey=ax1)
columns = [
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
name for name in conversion_history.index.names if name not in ['dt']
]
filtered_data = conversion_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax2)
plt.xlabel('Дата привлечения')
plt.title('Динамика конверсии пользователей на {}-й день'.format(horizon))
plt.tight_layout()
plt.show()
# функция для визуализации LTV и ROI
def plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon, window=7):
# задаём сетку отрисовки графиков
plt.figure(figsize=(20, 10))
# из таблицы ltv исключаем размеры когорт
ltv = ltv.drop(columns=['cohort_size'])
# в таблице динамики ltv оставляем только нужный лайфтайм
ltv_history = ltv_history.drop(columns=['cohort_size'])[[horizon - 1]]
# стоимость привлечения запишем в отдельный фрейм
cac_history = roi_history[['cac']]
# из таблицы roi исключаем размеры когорт и cac
roi = roi.drop(columns=['cohort_size', 'cac'])
# в таблице динамики roi оставляем только нужный лайфтайм
roi_history = roi_history.drop(columns=['cohort_size', 'cac'])[
[horizon - 1]
]
# первый график — кривые ltv
ax1 = plt.subplot(2, 3, 1)
ltv.T.plot(grid=True, ax=ax1)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('LTV')
# второй график — динамика ltv
ax2 = plt.subplot(2, 3, 2, sharey=ax1)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in ltv_history.index.names if name not in ['dt']]
filtered_data = ltv_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax2)
plt.xlabel('Дата привлечения')
plt.title('Динамика LTV пользователей на {}-й день'.format(horizon))
# третий график — динамика cac
ax3 = plt.subplot(2, 3, 3, sharey=ax1)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in cac_history.index.names if name not in ['dt']]
filtered_data = cac_history.pivot_table(
index='dt', columns=columns, values='cac', aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax3)
plt.xlabel('Дата привлечения')
plt.title('Динамика стоимости привлечения пользователей')
# четвёртый график — кривые roi
ax4 = plt.subplot(2, 3, 4)
roi.T.plot(grid=True, ax=ax4)
plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('ROI')
# пятый график — динамика roi
ax5 = plt.subplot(2, 3, 5, sharey=ax4)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in roi_history.index.names if name not in ['dt']]
filtered_data = roi_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax5)
plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
plt.xlabel('Дата привлечения')
plt.title('Динамика ROI пользователей на {}-й день'.format(horizon))
plt.tight_layout()
plt.show()
Задали функции для:
1.вычисления значений метрик:
# получаем профили пользователей
profiles = get_profiles(visits, orders, costs)
profiles.head(5)
| user_id | first_ts | channel | device | region | dt | month | payer | acquisition_cost | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 599326 | 2019-05-07 20:58:57 | FaceBoom | Mac | United States | 2019-05-07 | 2019-05-01 | True | 1.088172 |
| 1 | 4919697 | 2019-07-09 12:46:07 | FaceBoom | iPhone | United States | 2019-07-09 | 2019-07-01 | False | 1.107237 |
| 2 | 6085896 | 2019-10-01 09:58:33 | organic | iPhone | France | 2019-10-01 | 2019-10-01 | False | 0.000000 |
| 3 | 22593348 | 2019-08-22 21:35:48 | AdNonSense | PC | Germany | 2019-08-22 | 2019-08-01 | False | 0.988235 |
| 4 | 31989216 | 2019-10-02 00:07:44 | YRabbit | iPhone | United States | 2019-10-02 | 2019-10-01 | False | 0.230769 |
# минимальная дата привлечения пользователей
min_analysis_date = costs['dt'].min()
min_analysis_date
datetime.date(2019, 5, 1)
# максимальная дата привлечения пользователей
max_analysis_date = costs['dt'].max()
max_analysis_date
datetime.date(2019, 10, 27)
# строим таблицу, отражающую количество пользователей и долю платящих из каждой страны
region_payer = profiles.groupby('region', as_index=False)\
.agg({'user_id': 'nunique', 'payer': ['sum', 'mean']}, as_index=False)\
.sort_values(by='region', ascending=False)
region_payer.columns = ['region','user_id', 'payer', 'share_payer']
region_payer.style.format({'share_payer': '{:.2%}'})
| region | user_id | payer | share_payer | |
|---|---|---|---|---|
| 3 | United States | 100002 | 6902 | 6.90% |
| 2 | UK | 17575 | 700 | 3.98% |
| 1 | Germany | 14981 | 616 | 4.11% |
| 0 | France | 17450 | 663 | 3.80% |
# строим круговую диаграмму по количеству платежеспособных пользователей с разбивкой по странам
plt.pie(region_payer['payer'], labels = (region_payer['region']), autopct='%1.2f%%')
plt.title('Страны')
plt.show();
Количество платежеспособных пользователей от общего количества пользователей составляет 5.9 % (8881/150008).
Самые платежеспособные пользователи из USA, их доля составляет 6.9 % (6902/100002) от общего количества пользователей, доля остальных стран: Germany, UK, France составляет около 4 %.
Доля платежеспособных пользователей от общего количества пользователей USA составляет 77.72 % (6902 100 / 8881). Остальная доля платежеспособных пользователей от общего количества пользователей составляет 22.28 % и распределилась по странам:Germany-6.94 % (616 100 / 8881), UK-7.88 % (700 100 / 8881), France- 7.46 % (663 100 / 8881).
# строим таблицу, отражающую количество пользователей и долю платящих для каждого устройства
device_payer = profiles.groupby('device', as_index=False)\
.agg({'user_id': 'nunique', 'payer': ['sum', 'mean']}, as_index=False)\
.sort_values(by='device', ascending=False)
device_payer.columns = ['device','user_id', 'payer', 'share_payer']
device_payer.style.format({'share_payer': '{:.2%}'})
| device | user_id | payer | share_payer | |
|---|---|---|---|---|
| 3 | iPhone | 54479 | 3382 | 6.21% |
| 2 | PC | 30455 | 1537 | 5.05% |
| 1 | Mac | 30042 | 1912 | 6.36% |
| 0 | Android | 35032 | 2050 | 5.85% |
# строим круговую диаграмму по количеству платежеспособных пользователей с разбивкой по каждому устройству
plt.pie(device_payer['payer'], labels = (device_payer['device']), autopct='%1.2f%%')
plt.title('Устройства')
plt.show();
Количество платежеспособных пользователей от общего количества пользователей составляет 5.9 % (8881/150008).
Платежеспособные пользователи предпочитают устройства: Mac - 6.36 % (1912/30042), iPhone - 6.21 % (3382/54479).
Доля платежеспособных пользователей от общего количества пользователей по устройствам составляет: iPhone - 38.08 % (3382 100 / 8881), Android - 23. 08 % (2050 100 / 8881), Mac - 21.53 % (1912 100 / 8881), PC - 17.31 % (1537 100 / 8881).
# строим таблицу, отражающую количество пользователей и долю платящих из каждой канала привлечения
channel_payer = profiles.groupby('channel', as_index=False)\
.agg({'user_id': 'nunique', 'payer': ['sum', 'mean']},as_index=False)\
.sort_values(by='channel', ascending=False)
channel_payer.columns = ['channel','user_id', 'payer', 'share_payer']
channel_payer.style.format({'share_payer': '{:.2%}'})
| channel | user_id | payer | share_payer | |
|---|---|---|---|---|
| 10 | organic | 56439 | 1160 | 2.06% |
| 9 | lambdaMediaAds | 2149 | 225 | 10.47% |
| 8 | YRabbit | 4312 | 165 | 3.83% |
| 7 | WahooNetBanner | 8553 | 453 | 5.30% |
| 6 | TipTop | 19561 | 1878 | 9.60% |
| 5 | RocketSuperAds | 4448 | 352 | 7.91% |
| 4 | OppleCreativeMedia | 8605 | 233 | 2.71% |
| 3 | MediaTornado | 4364 | 156 | 3.57% |
| 2 | LeapBob | 8553 | 262 | 3.06% |
| 1 | FaceBoom | 29144 | 3557 | 12.20% |
| 0 | AdNonSense | 3880 | 440 | 11.34% |
# строим круговую диаграмму по количеству платежеспособных пользователей с разбивкой по канала привлечения
plt.pie(channel_payer['payer'], labels = (channel_payer['channel']), autopct='%1.2f%%')
plt.title('Каналы привлечения')
plt.show();
Количество платежеспособных пользователей от общего количества пользователей составляет 5.9 % (8881/150008).
Платежеспособные пользователи предпочитают каналы привлечения: FaceBoom - 12.20 % (3557/29144), AdNonSense - 11.34 % (440/3880), lambdaMediaAds - 10.47 % (225/2149), TipTop - 9.60 % (1878/19561), RocketSuperAds - 9.60 % (1878/19561).
Доля платежеспособных пользователей от общего количества пользователей по каналу привлечения составляет: FaceBoom - 40.05 % (3557 100 / 8881), TipTop - 21.15 % (1878 100 / 8881).
Вывод: Платежеспособные пользователи из USA предпочитают устройства Mac - 6.36 % , iPhone - 6.21 % с каналом привлечения FaceBoom - 12.20%, AdNonSense - 11.34 %, lambdaMediaAds - 10.47 %, TipTop - 9.60 % , RocketSuperAds - 9.60 % .
profiles
| user_id | first_ts | channel | device | region | dt | month | payer | acquisition_cost | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 599326 | 2019-05-07 20:58:57 | FaceBoom | Mac | United States | 2019-05-07 | 2019-05-01 | True | 1.088172 |
| 1 | 4919697 | 2019-07-09 12:46:07 | FaceBoom | iPhone | United States | 2019-07-09 | 2019-07-01 | False | 1.107237 |
| 2 | 6085896 | 2019-10-01 09:58:33 | organic | iPhone | France | 2019-10-01 | 2019-10-01 | False | 0.000000 |
| 3 | 22593348 | 2019-08-22 21:35:48 | AdNonSense | PC | Germany | 2019-08-22 | 2019-08-01 | False | 0.988235 |
| 4 | 31989216 | 2019-10-02 00:07:44 | YRabbit | iPhone | United States | 2019-10-02 | 2019-10-01 | False | 0.230769 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 150003 | 999956196527 | 2019-09-28 08:33:02 | TipTop | iPhone | United States | 2019-09-28 | 2019-09-01 | False | 3.500000 |
| 150004 | 999975439887 | 2019-10-21 00:35:17 | organic | PC | UK | 2019-10-21 | 2019-10-01 | False | 0.000000 |
| 150005 | 999976332130 | 2019-07-23 02:57:06 | TipTop | iPhone | United States | 2019-07-23 | 2019-07-01 | False | 2.600000 |
| 150006 | 999979924135 | 2019-09-28 21:28:09 | MediaTornado | PC | United States | 2019-09-28 | 2019-09-01 | False | 0.205714 |
| 150007 | 999999563947 | 2019-10-18 19:57:25 | organic | iPhone | United States | 2019-10-18 | 2019-10-01 | False | 0.000000 |
150008 rows × 9 columns
#считаем общую сумму расходов на маркетинг
profiles['acquisition_cost'].sum().round(2)
105497.3
# считаем общее количество пользователей
profiles[{'user_id': 'nunique'}].count()
C:\Users\Tima\AppData\Local\Temp\ipykernel_10564\2576958088.py:2: FutureWarning: Passing a dict as an indexer is deprecated and will raise in a future version. Use a list instead.
profiles[{'user_id': 'nunique'}].count()
user_id 150008 dtype: int64
# строим таблицу распределения трат по рекламным источникам
cost_channel = profiles.groupby('channel', as_index=False)\
.agg({'user_id': 'nunique', 'acquisition_cost': ['sum']},as_index=False)\
.sort_values(by='channel', ascending=False)
cost_channel.columns = ['channel','user_id', 'acquisition_cost']
cost_channel
| channel | user_id | acquisition_cost | |
|---|---|---|---|
| 10 | organic | 56439 | 0.00 |
| 9 | lambdaMediaAds | 2149 | 1557.60 |
| 8 | YRabbit | 4312 | 944.22 |
| 7 | WahooNetBanner | 8553 | 5151.00 |
| 6 | TipTop | 19561 | 54751.30 |
| 5 | RocketSuperAds | 4448 | 1833.00 |
| 4 | OppleCreativeMedia | 8605 | 2151.25 |
| 3 | MediaTornado | 4364 | 954.48 |
| 2 | LeapBob | 8553 | 1797.60 |
| 1 | FaceBoom | 29144 | 32445.60 |
| 0 | AdNonSense | 3880 | 3911.25 |
# выделяем номер месяца и номер недели
profiles['profiles_month'] = pd.to_datetime(profiles['dt']).dt.month
profiles['profiles_week'] = pd.to_datetime(profiles['dt']).dt.isocalendar().week
# строим график динамики изменения расходов во времени (по неделям и месяцам) по каждому источнику без сглаживания данных
profiles.pivot_table(index=['profiles_month', 'profiles_week'], \
columns = 'channel', values = 'acquisition_cost', aggfunc = 'sum')\
.plot(grid=True, figsize=(15, 5))
plt.ylabel('Расходы на рекламную компанию, $')
plt.xlabel('Дата привлечения (месяц, неделя)')
plt.title('Динамика рекламных расходов по каналам привлечения')
plt.legend(bbox_to_anchor=(1.02, 1.0), loc='best')
plt.show()
# строим график динамики изменения расходов во времени (по неделям и месяцам) по каждому источнику с сглаживанием данных
month_week = profiles.pivot_table(index=['profiles_month', 'profiles_week'], columns = 'channel', values = 'acquisition_cost', aggfunc = 'sum')
month_week = month_week.rolling(7).sum()
month_week.plot(grid=True, figsize=(15, 5))
plt.ylabel('Расходы на рекламную компанию, $')
plt.xlabel('Дата привлечения (месяц, неделя)')
plt.title('Динамика рекламных расходов по каналам привлечения')
plt.legend(bbox_to_anchor=(1.02, 1.0), loc='best')
plt.show()
Больше всего рекламных расходов по каналам привлечения: TipTop (54751.30), FaceBoom(32445.60). Затраты на рекламу по каналам привлечения WahooNetBanner, AdNonSense равномерные. В сентябре на 35, 40 неделях затраты на TipTop, FaceBoom резко снижаются, а затем резко растут.
# строим таблицу распределения трат по рекламным источникам со стоимостью привлечения новых пользователей
cost_channel_organic = profiles.query('channel != "organic"')\
.groupby('channel', as_index=False)\
.agg({'user_id': 'nunique', 'acquisition_cost': ['sum', 'mean']},as_index=False)\
.sort_values(by='channel', ascending=False)
cost_channel_organic.columns = ['channel','user_id', 'acquisition_cost', 'cac']
cost_channel_organic.style.format({'acquisition_cost': '{:.2f}','cac': '{:.2f}'})
| channel | user_id | acquisition_cost | cac | |
|---|---|---|---|---|
| 9 | lambdaMediaAds | 2149 | 1557.60 | 0.72 |
| 8 | YRabbit | 4312 | 944.22 | 0.22 |
| 7 | WahooNetBanner | 8553 | 5151.00 | 0.60 |
| 6 | TipTop | 19561 | 54751.30 | 2.80 |
| 5 | RocketSuperAds | 4448 | 1833.00 | 0.41 |
| 4 | OppleCreativeMedia | 8605 | 2151.25 | 0.25 |
| 3 | MediaTornado | 4364 | 954.48 | 0.22 |
| 2 | LeapBob | 8553 | 1797.60 | 0.21 |
| 1 | FaceBoom | 29144 | 32445.60 | 1.11 |
| 0 | AdNonSense | 3880 | 3911.25 | 1.01 |
Наибольшей стоимостью привлечения новых пользователей по рекламным источникам является TipTop - 2.80.
Вывод: Всего на маркетинг потрачено 105497.3 у.е. Больше всего рекламных расходов по каналам привлечения: TipTop (54751.30), FaceBoom(32445.60). Наибольшей стоимостью привлечения новых пользователей по рекламным источникам является TipTop - 2.80.
#считаем среднее CAC
avg_cac = cost_channel_organic['acquisition_cost'] /cost_channel_organic['user_id'].mean()
round(avg_cac.mean(),2)
1.13
Используя графики LTV, ROI и CAC, проанализируйте окупаемость рекламы. Считайте, что на календаре 1 ноября 2019 года, а в бизнес-плане заложено, что пользователи должны окупаться не позднее чем через две недели после привлечения. Необходимость включения в анализ органических пользователей определите самостоятельно.
# задаем момент и горизонт анализа
observation_date = datetime(2019, 11, 1).date() # момент анализа
horizon_days = 14 # горизонт анализа
# максимальная дата привлечения
max_horizon_days = observation_date - timedelta(days=horizon_days - 1)
print(max_horizon_days)
2019-10-19
#исключим пользователей с органическим трафиком
profiles = profiles.query('channel != "organic"')
# считаем LTV и ROI
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles, orders, observation_date, horizon_days
)
# строим графики
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days)
По графикам LTV и ROI можно сделать выводы:
# смотрим конверсию
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles, orders, observation_date, horizon_days
)
# строим график
plot_conversion(conversion_grouped, conversion_history, horizon_days)
# смотрим конверсию с разбивкой по устройствам
dimensions = ['device']
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles, orders, observation_date, horizon_days, dimensions = dimensions
)
# строим график
plot_conversion(conversion_grouped, conversion_history, horizon_days)
Конверсия выше всего с разбивкой по устройствам Android, Mac, iPhone.Конверсия подвержена сезонной зависимости.
# смотрим конверсию с разбивкой по странам
dimensions = ['region']
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles, orders, observation_date, horizon_days, dimensions = dimensions
)
# строим график
plot_conversion(conversion_grouped, conversion_history, horizon_days)
Конверсия выше всего с разбивкой по странам в США.Конверсия подвержена сезонной зависимости.
# смотрим конверсию с разбивкой по источникам привлечения
dimensions = ['channel']
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles, orders, observation_date, horizon_days, dimensions = dimensions
)
# строим график
plot_conversion(conversion_grouped, conversion_history, horizon_days)
Конверсия выше всего с разбивкой по источникам привлечения FaceBoom, AdNonSense, lambdaMediaAds, TipTop.Конверсия подвержена сезонной зависимости.
Конверсия не падает — значит, при расчёте были верно определены момент и горизонт анализа. Количество новых покупателей не может быть меньше значения предыдущего дня, ведь при расчёте используется сумма с накоплением. Выше всего конверсия: с разбивкой по странам в США; с разбивкой по устройствам Android, Mac, iPhone; по источникам привлечения FaceBoom, AdNonSense, lambdaMediaAds, TipTop.Конверсия подвержена сезонной зависимости.
# смотрим удержание
retention_raw, retention_grouped, retention_history = get_retention(
profiles, visits, observation_date, horizon_days
)
# строим график
plot_retention(retention_grouped, retention_history, horizon_days)
# смотрим удержание с разбивкой по устройствам
dimensions = ['device']
retention_raw, retention_grouped, retention_history = get_retention(
profiles, visits, observation_date, horizon_days, dimensions = dimensions
)
# строим график
plot_retention(retention_grouped, retention_history, horizon_days)
Лучше всего удерживается с разбивкой по устройствам PC, хуже Android, Mac, iPhone. Удержание подвержено сезонной зависимости.
# смотрим удержание с разбивкой по странам
dimensions = ['region']
retention_raw, retention_grouped, retention_history = get_retention(
profiles, visits, observation_date, horizon_days, dimensions = dimensions
)
# строим график
plot_retention(retention_grouped, retention_history, horizon_days)
Лучше всего удерживается с разбивкой по странам UK, Germany, France, хуже USA. Удержание подвержено сезонной зависимости.
# смотрим удержание с разбивкой по источникам привлечения
dimensions = ['channel']
retention_raw, retention_grouped, retention_history = get_retention(
profiles, visits, observation_date, horizon_days, dimensions = dimensions
)
# строим график
plot_retention(retention_grouped, retention_history, horizon_days)
Удержание платящих пользователей с разбивкой по источникам привлечения одинакого, кроме FaceBoom, AdNonSense. Удержание подвержено сезонной зависимости.
Retention Rate - коэффициент удержания со временем падает как у платящих пользователей, так и не платящих пользователей. Это нормально, так как коэффициент удержания показывает, сколько пользователей из когорты относительно их изначального числа вернулись, то есть воспользовались продуктом или услугой, в последующие периоды.
Лучше всего удерживается с разбивкой по устройствам PC, хуже Android, Mac, iPhone. Лучше всего удерживается с разбивкой по странам UK, Germany, France, хуже USA. Удержание платящих пользователей с разбивкой по источникам привлечения одинакого, кроме FaceBoom, AdNonSense.У платящих пользователей выявлена сезонная зависимость.
# смотрим окупаемость с разбивкой по устройствам
dimensions = ['device']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles, orders, observation_date, horizon_days, dimensions=dimensions
)
# строим график
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
График с разбивкой по устройствам показывает:
# смотрим окупаемость с разбивкой по странам
dimensions = ['region']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles, orders, observation_date, horizon_days, dimensions=dimensions
)
# строим график
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
График с разбивкой по странам показывает:
# смотрим окупаемость с разбивкой по источникам привлечения
dimensions = ['channel']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles, orders, observation_date, horizon_days, dimensions=dimensions
)
# строим график
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
График с разбивкой по источникам привлечения показывает:
Вывод:
Проблема окупаемости вызвана с увеличением расходов на рекламу и малым количеством привлечения пользователей.
# оставляем в таблице США
profiles_usa = profiles.query('region == "United States"')
# смотрим окупаемость с разбивкой по источникам привлечения
dimensions = ['channel']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles_usa, orders, observation_date, horizon_days, dimensions=dimensions
)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
В США динамика стоимости привлечения пользователей через TipTop была значительно выше, чем у других каналов, но расходы на рекламу не окупились.
# смотрим окупаемость с разбивкой по устройствам по стране США
dimensions = ['device']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles_usa, orders, observation_date, horizon_days, dimensions=dimensions
)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
В США динамика стоимости привлечения пользователей была практически одинаковой для всех устройств. Ни одно устройство не окупилось.
# исключаем из таблицы США
profiles_not_usa = profiles.query('region != "United States"')
# смотрим окупаемость с разбивкой по источникам привлечения без США
dimensions = ['channel']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles_not_usa, orders, observation_date, horizon_days, dimensions=dimensions
)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
График с разбивкой по источникам привлечения показывает:
# смотрим окупаемость с разбивкой по устройствам без США
dimensions = ['device']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles_not_usa, orders, observation_date, horizon_days, dimensions=dimensions
)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
Окупаются все устройства, LTV всё так же подвержен сезонности, но стабилен, CAC уменьшается по всем устройствам.
Еще раз убедились, что все проблемы в США. Все бизнес-паказатели пришли в норму. Реклама окупается по всем устройствам и источникам привлечения. Кроме канала привлечения AdNonSense.
Вывод:
В исследовании проанализированы данные по пользователям развлекательного приложения Procrastinate Pro+ из США, Великобритания, Франция, Германия., разделенные на категории устройств (iPhone, Android, PC, Mac) и разных каналах привлечения. В данный момент на привлечение пользователей реклама не окупается, потому что расходы на рекламу составляют 105497.30 у.е., привлечено пользователей 150008 у.е. Убыток составляет 44510,70 у.е.Расходы на рекламу растут, а динамика привлечения пользователей не меняется.
Большую долю платежеспособных пользователей от общего количества пользователей занимает США(77.72%), которая оказывает негативное влияние на окупаемость приложения. Неоправданные расходы на рекламу были направлены на устройства: Mac, iPhone по источникам привлечения TipTop.
Рекомендации
Необходимо сократить финансирование на рекламу в USA устройств iPhone, Mac по каналам привлечения TipTop. Снизить расходы на рекламу по каналам привлечения FaceBoom и AdNonSense, т.к. ежемесячные траты на рекламу стабильны, но это не помогает привлечь новых пользователей. Увеличить рекламу устройств Android, PC для привлечения новых пользователей.